{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Part 1, Topic 3, Lab A: DFA Attack Against Last MixColumns\n",
    "\n",
    "**SUMMARY:** *Our previous fault attacks have been very implementation specific, even requiring that the compiler lay things out in a specific way. While it wouldn't be that unexpected for them to work against another target, we can actually do a fault attack that's much easier in practice. All it requires is that we insert a single byte fault between two operations near the end of AES.*\n",
    "\n",
    "*In this lab, we'll be covering the theory behind the attack.*\n",
    "\n",
    "##  DFA Fault Theory\n",
    "\n",
    "There's a great article over at https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html, if you're interested in the actual analysis. If not, here's a quick TLDR that skips as much math as possible:\n",
    "\n",
    "Our goal is to insert faults between the last two MixColumn operations. As a reminder, here's a block diagram of AES:\n",
    "\n",
    "![](img/aes_operations.png)\n",
    "(source: http://www.iis.ee.ethz.ch/~kgf/acacia/fig/aes.png)\n",
    "\n",
    "This results in:\n",
    "\n",
    "1. 2 sets of ciphertext outputs with the same plaintext - one $O$ with no errors, and one faulted output $O^\\prime$\n",
    "1. XORing the outputs will result in the following system of equations:\n",
    "$$\\space \\\\\n",
    "O_0 + O_0^\\prime = S(Y_0) + S(2Z + Y_0) \\\\\n",
    "O_7 + O_7^\\prime = S(Y_1) + S(3Z + Y_1) \\\\\n",
    "O_{10} + O_{10}^\\prime = S(Y_2) + S(Z + Y_2) \\\\\n",
    "O_{13} + O_{13}^\\prime = S(Y_3) + S(2Z + Y_3) \\\\\n",
    "$$\n",
    "1. Solving these equations will result in a set of $Y_n$ and $Z$. Here $Y_n$ is the non faulted output of Mix Columns and Add Round Key for a single column (aka the input to the final round). $Z$ is the faulted version of the first byte XORd with the non faulted version of the byte, so $aZ + Y_n$ is just the faulted version o $Y_n$. $S(x)$ is the SBox operation, $+$ is an XOR, and multiplications are done in $GF(2^8)$ (we've got a special `gmul()` function to do this for us).\n",
    "1. $Y_n$ is constant between faults with the same plaintext (it's only made up of non-faulted bytes so faults have no effect on it) - another fault is enough to narrow $Y_n$ down to one value per byte\n",
    "1. $Y_n$ can then be turned into 4 key bytes with the following equations:\n",
    "$$\\space\\\\\n",
    "\\begin{aligned}\n",
    "K_{10,0} &=S\\left(Y_{0}\\right)+O_{0} \\\\\n",
    "K_{10,7} &=S\\left(Y_{1}\\right)+ O_{7}\\\\\n",
    "K_{10,10} &=S\\left(Y_{2}\\right)+ O_{10}\\\\\n",
    "K_{10,13} &=S\\left(Y_{3}\\right)+O_{13}\n",
    "\\end{aligned}$$\n",
    "\n",
    "\n",
    "The first system of equations is non-linear with multiple solutions, so it's going to be much easier to just brute force it -  aka try every possible $Z$, $Y_0$, $Y_1$, $Y_2$, and $Y_3$ value in these equations, taking only the ones that work for all the equations. You can make this much faster by short circuiting - as soon as it fails one of these equations, there's no need to continue on from that point. For example, if you're going through the equations in the above sequence and the second one fails, there's no need to continue on with $Y_2$ and $Y_3$ for that particular combination of $Z$, $Y_0$, and $Y_1$.\n",
    "\n",
    "We can test this theory by stopping AES just before the 9th round mix columns and changing the 0th byte to a random value. ChipWhisperer includes an AES Cipher that we can use to do the encryption."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# get an AES cipher\n",
    "import chipwhisperer as cw\n",
    "from chipwhisperer.common.utils.aes_cipher import AESCipher, aes_tables\n",
    "import chipwhisperer.analyzer as cwa\n",
    "ktp = cw.ktp.Basic()\n",
    "key = list(ktp.next()[0])\n",
    "for i in range(10):\n",
    "    key.extend(cwa.aes_funcs.key_schedule_rounds(key[0:16], 0, i+1))\n",
    "    \n",
    "cipher = AESCipher(key)\n",
    "print(bytearray(key))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Just to verify that our cipher works, let's use Pycryptodome to also do an encryption. We can then compare the results to make sure they match."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# verify that our cipher works\n",
    "from Crypto.Cipher import AES\n",
    "check_cipher = AES.new(ktp.next()[0], AES.MODE_ECB)\n",
    "\n",
    "#generate random plaintext\n",
    "pt = ktp.next()[1]\n",
    "\n",
    "#encrypt with both\n",
    "ct1 = cipher.cipher_block(list(pt))\n",
    "ct2 = check_cipher.encrypt(pt)\n",
    "\n",
    "# verify that outputs are the same\n",
    "bytearray(ct1) == bytearray(ct2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can generate our glitch by doing the AES encryption, stopping at the 9th round mix columns, and changing a value, then completing the encryption. We also make a copy of the state before we insert the fault and complete AES on that as well."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import random\n",
    "\n",
    "def generate_glitch(pt, cipher):\n",
    "    # Do AES, but stop before the last Mix Columns\n",
    "    state = list(pt)\n",
    "    state = state+[16-len(state)]*(16-len(state))\n",
    "    cipher._add_round_key(state, 0)\n",
    "    for i in range(1, 9):\n",
    "        cipher._sub_bytes(state)\n",
    "        cipher._shift_rows(state)\n",
    "        cipher._mix_columns(state, False)\n",
    "        cipher._add_round_key(state, i)\n",
    "    cipher._sub_bytes(state)\n",
    "    cipher._shift_rows(state)\n",
    "\n",
    "    # Make a copy of the state and finish the rest of AES\n",
    "    x = list(state)\n",
    "    cipher._mix_columns(x, False)\n",
    "    cipher._add_round_key(x, 9)\n",
    "    cipher._sub_bytes(x)\n",
    "    cipher._shift_rows(x)\n",
    "    cipher._add_round_key(x, 10)\n",
    "\n",
    "    # Insert a fault and go through the rest of AES with the fault\n",
    "    import random\n",
    "    random.seed()\n",
    "    fault = random.getrandbits(8)\n",
    "    if state[0] == fault:\n",
    "        fault += 1\n",
    "        fault = fault % 0xFF\n",
    "    \n",
    "    state[0] = fault\n",
    "    cipher._mix_columns(state, False)\n",
    "    cipher._add_round_key(state, 9)\n",
    "    cipher._sub_bytes(state)\n",
    "    cipher._shift_rows(state)\n",
    "    cipher._add_round_key(state, 10)\n",
    "    return state, x\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's generate some good and faulty output:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# uncomment to generate a random plaintext\n",
    "#pt = ktp.next()[1] \n",
    "\n",
    "#get two outputs, a normal and a faulty one\n",
    "O_fault, O_good = generate_glitch(pt, cipher) \n",
    "\n",
    "#should be the same except for bytes 0, 7, 10, 13\n",
    "print(bytearray(O_fault), bytearray(O_good)) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's our code to brute force the Y values, update our Y's with new Y values, and convert a Y value into a key. `gmul()` performs a multiply over $GF(2^8)$, and `check_Y()` checks that a given Y and Z fulfill the requirements of our system of equations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "from tqdm.notebook import trange\n",
    "def get_Y_guesses(state, x):\n",
    "    #GF(2^8) multiplication adapted from https://en.wikipedia.org/wiki/Finite_field_arithmetic#C_programming_example\n",
    "    def gmul(a, b): \n",
    "        p = 0\n",
    "        while a and b:\n",
    "            if b & 1:\n",
    "                p ^= a\n",
    "            if (a & 0x80):\n",
    "                a = (a << 1) ^ 0x11b;\n",
    "            else:\n",
    "                a <<= 1\n",
    "            b >>= 1\n",
    "        return p\n",
    "    \n",
    "    #check that Yn and Z fulfill requirements\n",
    "    def check_Y(Z, Yn, n):\n",
    "        lookup = [0, 7, 10, 13]\n",
    "        lhs = state[lookup[n]] ^ x[lookup[n]]\n",
    "        coeff = [2, 3, 1, 1]\n",
    "        rhs = aes_tables.sbox[Yn] ^ aes_tables.sbox[gmul(Z, coeff[n]) ^ Yn]\n",
    "        return lhs == rhs\n",
    "    guesses = []\n",
    "    \n",
    "    # brute force Z and Yn\n",
    "    for Z in trange(255):\n",
    "        for Y0 in range(255):\n",
    "            if check_Y(Z, Y0, 0):\n",
    "                for Y1 in range(255):\n",
    "                    if check_Y(Z, Y1, 1):\n",
    "                        for Y2 in range(255):\n",
    "                            if check_Y(Z, Y2, 2):\n",
    "                                for Y3 in range(255):\n",
    "                                    if check_Y(Z, Y3, 3):\n",
    "                                        guesses.append((Y0, Y1, Y2, Y3))\n",
    "    return guesses\n",
    "    \n",
    "def update_Y_guesses(Y_old, Y_new):\n",
    "    updated_Y = []\n",
    "    for Ys in Y_old:\n",
    "        if Ys in Y_new:\n",
    "            updated_Y.append(Ys)\n",
    "    return updated_Y\n",
    "\n",
    "def Y_to_key(x, Y):\n",
    "    return aes_tables.sbox[Y] ^ x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can brute force the Y values:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# calculate Y_n for the outputs\n",
    "Y_guesses = get_Y_guesses(O_fault, O_good) \n",
    "#print(Y_guesses)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then generate a new fault, brute force Y values, and find the ones that match between the faults:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#get a new fault with the same plaintext (O_good will be the same)\n",
    "O_fault, O_good = generate_glitch(pt, cipher) \n",
    "print(bytearray(O_fault), bytearray(O_good)) \n",
    "\n",
    "# update our Y values with ones that also work for the new fault\n",
    "Y_guesses = update_Y_guesses(Y_guesses, get_Y_guesses(O_fault, O_good)) \n",
    "\n",
    "#should be left with one Y guess per\n",
    "print(Y_guesses)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can then print the key bytes that we recovered, as well as the bytes for the actual last round key:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#turn Y_n into key bytes\n",
    "lookup = [0, 7, 10, 13]\n",
    "print(\"bytes recovered: \", bytearray([Y_to_key(O_good[lookup[n]], Y_guesses[0][n]) for n in range(4)]))\n",
    "print(\"key bytes:       \", bytearray([key[160:][i] for i in lookup]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Extending the Attack\n",
    "\n",
    "There's a lot of scenarios that we didn't cover at all here:\n",
    "\n",
    "1. We only attacked one column of AES (aka 4 key bytes). A full attack will need to attack the rest of the columns as well, with the output lookup needing to change for each column.\n",
    "    * If we fault a single column of the previous round MixColumns, this will actually turn into a single byte fault for each column in the next round! MixColumns will spread the fault to each byte in the column. Each byte in the column is then placed in a separate column by the next ShiftRows.\n",
    "1. We assumed the fault was inserted in the first byte of the column. If the glitch was inserted at another byte in that column, the system of equations we solved changes. For a real attack, we don't know which byte we glitched, so we'd need to account for that in the attack. Depending on the implementation, you might also glitch multiple bytes in the same column, which you'd have to discard.\n",
    "1. We only did AES128 here. If this was AES256, we'd need to do the attack again for the previous round as well.\n",
    "\n",
    "Let's try updating the attack to also work if we fault a random byte in the column. We can update our function that brute forces the Y values to also take a fault_byte argument. Z is the only part that changes (remember, Y doesn't depend on anything to do with the fault!), so we can update our coefficient table to take a fault_byte argument as well.\n",
    "\n",
    "## Attacking Other Bytes\n",
    "\n",
    "If we want to take the other bytes, we'll actually have to look a bit more into the math (or not, feel free to skip this section if you don't care where these updated coefficients are coming from). If the AES column state is:\n",
    "\n",
    "$$\\left(\\begin{array}{llll}\n",
    "A & E & I & M \\\\\n",
    "B & F & J & N \\\\\n",
    "C & G & K & O \\\\\n",
    "D & H & L & P\n",
    "\\end{array}\\right)$$\n",
    "\n",
    "then $Y_n$ looks like:\n",
    "\n",
    "$$\n",
    "Y_{0}=2 A+3 B+C+D+K_{9,0} \\\\\n",
    "Y_{1}=3 A+B+C+2 D+K_{9,3} \\\\\n",
    "Y_{2}=A+B+2 C+3 D+K_{9,2} \\\\\n",
    "Y_{3}=A+2 B+3 C+D+K_{9,1}\n",
    "$$\n",
    "\n",
    "For the byte A attack, we needed to make the coefficient the same as the one in front of A for those equations. For the other bytes, we just need use the coefficient for that byte. For example, for B, instead of `[2, 3, 1, 1]`, we need to use `[3, 1, 1, 2]`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def get_Y_guesses(state, x, fault_byte):\n",
    "    #GF(2^8) multiplication adapted from https://en.wikipedia.org/wiki/Finite_field_arithmetic#C_programming_example\n",
    "    def gmul(a, b): \n",
    "        p = 0\n",
    "        while a and b:\n",
    "            if b & 1:\n",
    "                p ^= a\n",
    "            if (a & 0x80):\n",
    "                a = (a << 1) ^ 0x11b;\n",
    "            else:\n",
    "                a <<= 1\n",
    "            b >>= 1\n",
    "        return p\n",
    "    \n",
    "    #check that Yn and Z fulfill requirements\n",
    "    def check_Y(Z, Yn, n, fault_byte):\n",
    "        lookup = [0, 7, 10, 13]\n",
    "        lhs = state[lookup[n]] ^ x[lookup[n]]\n",
    "        coeff = [[2, 3, 1, 1], [3, 1, 1, 2], [1, 1, 2, 3], [1, 2, 3, 1]]\n",
    "        rhs = aes_tables.sbox[Yn] ^ aes_tables.sbox[gmul(Z, coeff[fault_byte][n]) ^ Yn]\n",
    "        return lhs == rhs\n",
    "    guesses = []\n",
    "    \n",
    "    # brute force Z and Yn\n",
    "    for Z in trange(255):\n",
    "        for Y0 in range(255):\n",
    "            if check_Y(Z, Y0, 0, fault_byte):\n",
    "                for Y1 in range(255):\n",
    "                    if check_Y(Z, Y1, 1, fault_byte):\n",
    "                        for Y2 in range(255):\n",
    "                            if check_Y(Z, Y2, 2, fault_byte):\n",
    "                                for Y3 in range(255):\n",
    "                                    if check_Y(Z, Y3, 3, fault_byte):\n",
    "                                        guesses.append((Y0, Y1, Y2, Y3, fault_byte))\n",
    "    return guesses"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gmul(a, b): \n",
    "    p = 0\n",
    "    while a and b:\n",
    "        if b & 1:\n",
    "            p ^= a\n",
    "        if (a & 0x80):\n",
    "            a = (a << 1) ^ 0x11b;\n",
    "        else:\n",
    "            a <<= 1\n",
    "        b >>= 1\n",
    "    return p"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = 0x60\n",
    "b = gmul(a, 2) ^ gmul(a, 3) ^ a ^ a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'0x60'"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "hex(b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We also need to update our glitch generation to randomly insert the glitch:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def generate_glitch(pt, cipher):\n",
    "    # Do AES, but stop before the last mix columns\n",
    "    state = list(pt)\n",
    "    state = state+[16-len(state)]*(16-len(state))\n",
    "    cipher._add_round_key(state, 0)\n",
    "    for i in range(1, 9):\n",
    "        cipher._sub_bytes(state)\n",
    "        cipher._shift_rows(state)\n",
    "        cipher._mix_columns(state, False)\n",
    "        cipher._add_round_key(state, i)\n",
    "    cipher._sub_bytes(state)\n",
    "    cipher._shift_rows(state)\n",
    "\n",
    "    # make a copy of the state and run it through the rest of AES\n",
    "    x = list(state)\n",
    "    cipher._mix_columns(x, False)\n",
    "    cipher._add_round_key(x, 9)\n",
    "    cipher._sub_bytes(x)\n",
    "    cipher._shift_rows(x)\n",
    "    cipher._add_round_key(x, 10)\n",
    "\n",
    "    # insert a random fault byte in a random location\n",
    "    import random\n",
    "    random.seed()\n",
    "    fault = random.getrandbits(8)\n",
    "    fault_byte = random.getrandbits(2)\n",
    "    if state[fault_byte] == fault:\n",
    "        fault += 1\n",
    "        fault = fault % 0xFF\n",
    "    state[fault_byte] = fault\n",
    "\n",
    "    #and take the faulted one through AES as well\n",
    "    cipher._mix_columns(state, False)\n",
    "    cipher._add_round_key(state, 9)\n",
    "    cipher._sub_bytes(state)\n",
    "    cipher._shift_rows(state)\n",
    "    cipher._add_round_key(state, 10)\n",
    "    return state, x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then we can just repeat what we did before:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#generate a random plaintext\n",
    "pt = ktp.next()[1] \n",
    "\n",
    "#get two outputs, a normal and a faulty one\n",
    "O_fault, O_good = generate_glitch(pt, cipher) \n",
    "\n",
    "#should be the same except for bytes 0, 7, 10, 13\n",
    "print(bytearray(O_fault), bytearray(O_good)) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And do the Y value brute force for each byte in the column."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Y_guesses = []\n",
    "for fault_byte in range(4):\n",
    "    Y_guesses.extend(get_Y_guesses(O_fault, O_good, fault_byte))\n",
    "    \n",
    "print(Y_guesses)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Our Y update function also has to change, since each fault could have a different byte (and we appended which byte was faulted to the Y values). We can also take the opportunity to print which bytes were faulted:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def update_Y_guesses(Y_old, Y_new):\n",
    "    updated_Y = []\n",
    "    for Ys in Y_old:\n",
    "        for Ys_new in Y_new:\n",
    "            if Ys[:-1] == Ys_new[:-1]:\n",
    "                updated_Y.append(Ys[:-1])\n",
    "                print(\"Fault in bytes {} and {}\".format(Ys[-1], Ys_new[-1]))\n",
    "    return updated_Y"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The rest is pretty similar to before. Generate a new glitch, brute force Y values, then match them to the old Y values."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#get a new fault with the same plaintext (O_good will be the same)\n",
    "O_fault, O_good = generate_glitch(pt, cipher) \n",
    "print(bytearray(O_fault), bytearray(O_good)) \n",
    "\n",
    "new_Y = []\n",
    "for fault_byte in range(4):\n",
    "    new_Y.extend(get_Y_guesses(O_fault, O_good, fault_byte))\n",
    "# update our Y values with ones that also work for the new fault\n",
    "Y_guesses = update_Y_guesses(Y_guesses, new_Y) \n",
    "\n",
    "#should be left with one Y guess per\n",
    "print(Y_guesses) \n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, only one combination of byte faults will result in a match: we only need 2 faults, even if we don't know which bytes were faulted!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#turn Y_n into key bytes\n",
    "lookup = [0, 7, 10, 13]\n",
    "print(\"bytes recovered: \", bytearray([Y_to_key(O_good[lookup[n]], Y_guesses[0][n]) for n in range(4)]))\n",
    "print(\"key bytes:       \", bytearray([key[160:][i] for i in lookup]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's everything together. Try running this block a few times to get glitches with different combinations of bytes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#generate a random plaintext\n",
    "pt = ktp.next()[1] \n",
    "\n",
    "#get two outputs, a normal and a faulty one\n",
    "O_fault, O_good = generate_glitch(pt, cipher) \n",
    "\n",
    "#should be the same except for bytes 0, 7, 10, 13\n",
    "print(bytearray(O_fault), bytearray(O_good)) \n",
    "\n",
    "Y_guesses = []\n",
    "for fault_byte in range(4):\n",
    "    Y_guesses.extend(get_Y_guesses(O_fault, O_good, fault_byte))\n",
    "\n",
    "#get a new fault with the same plaintext (O_good will be the same)\n",
    "O_fault, O_good = generate_glitch(pt, cipher) \n",
    "print(bytearray(O_fault), bytearray(O_good)) \n",
    "\n",
    "new_Y = []\n",
    "for fault_byte in range(4):\n",
    "    new_Y.extend(get_Y_guesses(O_fault, O_good, fault_byte))\n",
    "# update our Y values with ones that also work for the new fault\n",
    "Y_guesses = update_Y_guesses(Y_guesses, new_Y) \n",
    "\n",
    "#should be left with one Y guess per\n",
    "print(Y_guesses)\n",
    "\n",
    "#turn Y_n into key bytes\n",
    "lookup = [0, 7, 10, 13]\n",
    "print(\"bytes recovered: \", bytearray([Y_to_key(O_good[lookup[n]], Y_guesses[0][n]) for n in range(4)]))\n",
    "print(\"key bytes:       \", bytearray([key[160:][i] for i in lookup]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusions and Next Steps\n",
    "\n",
    "Compared to our earlier attacks, this attack is much more applicable to real hardware. Really, the only requirement besides being able to repeat the encryption operation with the same plaintext, being able to observe the ciphertext, and being able to introduce a single byte fault in a column (or a multi byte fault in a single column if we fault the 8th round instead). Given these requirements, it's possible to fault any implementation of AES.\n",
    "\n",
    "In the next tutorial, we'll look at doing this attack on real hardware and utilizing a library to do the analysis."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.4"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
